// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/device_impl.h"

#include <glog/logging.h>

#include "src/byte_counter.h"
#include "src/service.h"

using std::vector;

namespace cashew {

// Flimflam Device D-Bus identifiers
static const char *kFlimflamDeviceName = "org.chromium.flimflam";

// Flimflam Device property names
static const char *kFlimflamDeviceCarrierProperty = "Cellular.Carrier";
static const char *kFlimflamDeviceInterfaceProperty = "Interface";
static const char *kFlimflamDeviceTypeProperty = "Type";
static const char *kFlimflamDeviceIPConfigsProperty = "IPConfigs";

// Flimflam Device on-the-wire Type values
static const char *kFlimflamDeviceTypeEthernet = "ethernet";
static const char *kFlimflamDeviceTypeWifi = "wifi";
static const char *kFlimflamDeviceTypeWimax = "wimax";
static const char *kFlimflamDeviceTypeBluetooth = "bluetooth";
static const char *kFlimflamDeviceTypeGPS = "gps";
static const char *kFlimflamDeviceTypeCellular = "cellular";
// TODO(vlaviano): what does vendor look like?
static const char *kFlimflamDeviceTypeVendor = "";

// GetDeviceProperties retry interval
static const guint kSecondsPerMinute = 60;
static const guint kGetDevicePropertiesIntervalSeconds = 1 * kSecondsPerMinute;

DeviceImpl::DeviceImpl(Service * const parent,
                       DBus::Connection& connection,  // NOLINT
                       const DBus::Path& path)
    : DBus::ObjectProxy(connection, path, kFlimflamDeviceName),
      parent_(CHECK_NOTNULL(parent)), path_(path), type_(kTypeUnknown),
      get_properties_source_id_(0), retrying_get_properties_(false),
      byte_counter_(NULL), byte_counter_running_(false) {
  // schedule a GetProperties() call to our Flimflam service path to init state
  // we'll keep trying periodically until we succeed
  // we'll subsequently update this state by monitoring PropertyChanged signals
  get_properties_source_id_ =
      g_idle_add(StaticGetDevicePropertiesCallback, this);
  if (get_properties_source_id_ == 0) {
    LOG(ERROR) << path_ << ": ctor: g_idle_add failed";
  }
  property_changed_handler_.delegate(this);
}

DeviceImpl::~DeviceImpl() {
  StopByteCounter();
  if (get_properties_source_id_ != 0 &&
      !g_source_remove(get_properties_source_id_)) {
    LOG(WARNING) << path_ << ": dtor: g_source_remove failed";
  }
}

const DBus::Path& DeviceImpl::GetPath() const {
  return path_;
}

Device::Type DeviceImpl::GetType() const {
  return type_;
}

const std::string& DeviceImpl::GetCarrier() const {
  return carrier_;
}

const std::string& DeviceImpl::GetInterface() const {
  return interface_;
}

void DeviceImpl::GetNameServers(vector<std::string>* nameservers) const {
  if (ipconfig_ == NULL) {
    LOG(WARNING) << path_ << ": ipconfig_ == NULL in GetNameServers";
    return;
  }

  *nameservers = ipconfig_->GetNameServers();
}

// Flimflam Device D-Bus Proxy methods

void DeviceImpl::PropertyChanged(const std::string& property_name,
                                 const DBus::Variant& new_value) {
  LOG(INFO) << path_ << ": PropertyChanged: property_name = " << property_name;
  // Queue a tuple representing this signal for later processing from the glib
  // main loop. We do this to avoid libdbus-c++ deadlocks that can occur when
  // sending a dbus message from within a dbus callback like this one.
  PropertyChangedSignal signal(property_name, new_value);
  property_changed_handler_.EnqueueSignal(signal);
}

// PropertyChangedDelegate methods

void DeviceImpl::OnPropertyChanged(const PropertyChangedHandler *handler,
                                   const std::string& property_name,
                                   const DBus::Variant& new_value) {
  DCHECK(handler == &property_changed_handler_);
  LOG(INFO) << path_ << ": OnPropertyChanged: property_name = "
      << property_name;
  if (property_name == kFlimflamDeviceCarrierProperty) {
    OnCarrierUpdate(new_value.reader().get_string());
  } else if (property_name == kFlimflamDeviceInterfaceProperty) {
    OnInterfaceUpdate(new_value.reader().get_string());
  } else if (property_name == kFlimflamDeviceTypeProperty) {
    OnTypeUpdate(new_value.reader().get_string());
  } else if (property_name == kFlimflamDeviceIPConfigsProperty) {
    vector< ::DBus::Path> ipconfigs(new_value.operator vector< ::DBus::Path>());
    OnIPConfigsUpdate(ipconfigs);
  } else {
    // we don't care about this property
  }
}

// Service methods

bool DeviceImpl::StartByteCounter() {
  if (byte_counter_running_) {
    LOG(WARNING) << path_ << ": StartByteCounter: counter already running";
    return false;
  }
  byte_counter_running_ = true;
  if (interface_.empty()) {
    // if we don't yet know our interface name, we report success but hold off
    // creating a byte counter object; we'll do so later in OnInterfaceUpdate
    LOG(INFO) << path_
        << ": StartByteCounter: no interface name, delaying counter creation";
    return true;
  }
  if (!CreateByteCounter()) {
    LOG(WARNING) << path_
      << ": StartByteCounter: couldn't create byte counter for " << interface_;
    byte_counter_running_ = false;
    return false;
  }
  LOG(INFO) << path_ << ": StartByteCounter: created byte counter for "
      << interface_;
  return true;
}

void DeviceImpl::StopByteCounter() {
  if (byte_counter_running_) {
    // NOTE: |byte_counter_| is an instance of ProcfsByteCounter.
    // The implementation of ProcfsByteCounter is subtle in a way that
    // the destructor of ProcfsByteCounter may again trigger the invocation
    // of this method. To prevent |byte_counter_| from being deleted again
    // during its destruction, we must set |byte_counter_running_| to false
    // before calling DeleteByteCounter to delete |byte_counter_|.
    byte_counter_running_ = false;
    DeleteByteCounter();
    // notify |parent_|
    parent_->OnByteCounterStopped();
  }
}

bool DeviceImpl::ReadStats() {
  return byte_counter_ ? byte_counter_->ReadStats() : false;
}

bool DeviceImpl::GetByteCounterStats(uint64 *rx_bytes, uint64 *tx_bytes) const {
  CHECK(rx_bytes != NULL);
  CHECK(tx_bytes != NULL);

  if (!byte_counter_running_) {
    LOG(WARNING) << "GetByteCounterStats: byte counter is not running";
    return false;
  }

  *rx_bytes = byte_counter_->GetRxBytes();
  *tx_bytes = byte_counter_->GetTxBytes();

  return true;
}

bool DeviceImpl::ByteCounterRunning() const {
  return byte_counter_running_;
}

// ByteCounterDelegate methods

void DeviceImpl::OnByteCounterUpdate(const ByteCounter *counter,
                                     uint64 rx_bytes, uint64 tx_bytes) {
  DCHECK(byte_counter_running_);
  DCHECK(byte_counter_ != NULL);
  DCHECK(counter == byte_counter_);
  LOG(INFO) << path_ << ": OnByteCounterUpdate: rx_bytes = " << rx_bytes
      << ", tx_bytes = " << tx_bytes;
  parent_->OnByteCounterUpdate(rx_bytes, tx_bytes);
}

// Private methods

Device::Type DeviceImpl::TypeFromString(const std::string& type) const {
  if (type == kFlimflamDeviceTypeEthernet) {
    return kTypeEthernet;
  }
  if (type == kFlimflamDeviceTypeWifi) {
    return kTypeWifi;
  }
  if (type == kFlimflamDeviceTypeWimax) {
    return kTypeWimax;
  }
  if (type == kFlimflamDeviceTypeBluetooth) {
    return kTypeBluetooth;
  }
  if (type == kFlimflamDeviceTypeGPS) {
    return kTypeGPS;
  }
  if (type == kFlimflamDeviceTypeCellular) {
    return kTypeCellular;
  }
  if (type == kFlimflamDeviceTypeVendor) {
    return kTypeVendor;
  }
  return kTypeUnknown;
}

void DeviceImpl::OnCarrierUpdate(const std::string& carrier) {
  LOG(INFO) << path_ << ": OnCarrierUpdate: carrier = " << carrier;
  // only propagate this to parent Service if there's been a change
  // we expect to see only one such change (from "Unknown")
  if (carrier_ == carrier) {
    return;
  }
  if (!carrier_.empty()) {
    LOG(WARNING) << path_ << ": OnCarrierUpdate: carrier change from "
        << carrier_ << " to " << carrier << "!";
  }
  carrier_ = carrier;
  parent_->OnCarrierUpdate(carrier);
}

void DeviceImpl::OnInterfaceUpdate(const std::string& interface) {
  LOG(INFO) << path_ << ": OnInterfaceUpdate: interface = " << interface;
  if (interface_ == interface) {
    return;
  }
  if (!interface_.empty()) {
    LOG(WARNING) << path_ << ": OnInterfaceUpdate: interface change from "
        << interface_ << " to " << interface << "!";
  }
  const std::string old_interface = interface_;
  interface_ = interface;
  if (!byte_counter_running_) {
    return;
  }
  // get rid of byte counter for old interface
  StopByteCounter();
  // if we don't have new interface info, we can't make a new byte counter yet
  if (interface_.empty()) {
    return;
  }
  // create byte counter for new interface
  // TODO(vlaviano): creating new counter on interface change may confuse our
  // parent service.
  if (!StartByteCounter()) {
    LOG(ERROR) << path_
        << ": OnInterfaceUpdate: could not create byte counter for "
        << interface_;
    return;
  }
}

void DeviceImpl::OnTypeUpdate(const std::string& type) {
  LOG(INFO) << path_ << ": OnTypeUpdate: type = " << type;
  type_ = TypeFromString(type);
  if (type_ != kTypeCellular) {
    LOG(WARNING) << path_ << ": OnTypeUpdate: type is not cellular!";
  }
}

void DeviceImpl::OnIPConfigsUpdate(const vector< ::DBus::Path>& ipconfigs) {
  LOG(WARNING) << path_ << ": OnIPConfigsUpdate: " << ipconfigs.size()
               << " ipconfigs";
  // TODO(jglasgow): handle multiple IP configs
  for (vector< ::DBus::Path>::const_iterator it(ipconfigs.begin());
      it != ipconfigs.end();
      it++) {
    if (ipconfig_ != NULL && *it == ipconfig_->GetPath()) {
      LOG(INFO) << path_ << ": OnIPConfigsUpdate: Using existing ipconfig: "
                   << *it;
      break;
    }
    LOG(INFO) << path_ << ": OnIPConfigsUpdate: Using ipconfig at " << *it;
    ipconfig_.reset(Ipconfig::NewIpconfig(conn(), *it));
    break;
  }
}

// static
gboolean DeviceImpl::StaticGetDevicePropertiesCallback(gpointer data) {
  DeviceImpl *device = reinterpret_cast<DeviceImpl*>(data);
  CHECK_NOTNULL(device);
  DCHECK_NE(device->get_properties_source_id_, 0);
  if (!device->GetDeviceProperties()) {
    // call failed, so try again later
    LOG(WARNING) << device->GetPath()
        << ": StaticGetDevicePropertiesCallback: "
        << "GetDeviceProperties failed, will retry in "
        << kGetDevicePropertiesIntervalSeconds << " secs";
    // if we've arrived here from our first g_idle_add call, set up a timer
    if (!device->retrying_get_properties_) {
      guint source_id =
          g_timeout_add_seconds(kGetDevicePropertiesIntervalSeconds,
                                StaticGetDevicePropertiesCallback, device);
      device->get_properties_source_id_ = source_id;
      if (source_id == 0) {
          LOG(ERROR) << device->GetPath()
              << ": StaticGetDevicePropertiesCallback: "
              << "g_timeout_add_seconds failed";
        return FALSE;  // get rid of old glib source and give up
      }
      device->retrying_get_properties_ = true;
      // get rid of our old glib source, but our new timer will call us
      return FALSE;
    }
    // otherwise, our timer is already set up
    // return TRUE to indicate that we want to be called again later
    return TRUE;
  }
  device->get_properties_source_id_ = 0;
  device->retrying_get_properties_ = false;
  return FALSE;  // we succeeded, so don't repeat
}

bool DeviceImpl::GetDeviceProperties() {
  LOG(INFO) << path_ << ": GetDeviceProperties";
  PropertyMap properties;
  // dbus-c++ throws exceptions
  // invoke the "Existing Non-conformant Code" clause of the style guide and
  // isolate the rest of the system from this
  try {
    // TODO(vlaviano): make this call asynchronous
    properties = GetProperties();
  } catch (DBus::Error& error) {  // NOLINT
    LOG(WARNING) << path_
        << ": GetDeviceProperties: GetProperties() -> Exception: "
        << error.name() << ": " << error.message();
    return false;
  } catch (...) {  // NOLINT
    LOG(WARNING) << path_
        << ": GetDeviceProperties: GetProperties() -> Exception";
    return false;
  }
  LOG(INFO) << "Received " << properties.size() << " properties";

  // grab the properties in which we're interested
  PropertyMap::const_iterator it;
  it = properties.find(kFlimflamDeviceInterfaceProperty);
  if (it != properties.end()) {
    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
    OnInterfaceUpdate(value.reader().get_string());
  } else {
    LOG(WARNING) << path_ << ": GetDeviceProperties: no Interface property";
  }
  it = properties.find(kFlimflamDeviceTypeProperty);
  if (it != properties.end()) {
    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
    OnTypeUpdate(value.reader().get_string());
  } else {
    LOG(WARNING) << path_ << ": GetDeviceProperties: no Type property";
  }
  it = properties.find(kFlimflamDeviceIPConfigsProperty);
  if (it != properties.end()) {
    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
    vector< ::DBus::Path> ipconfigs(value.operator vector< ::DBus::Path>());
    OnIPConfigsUpdate(ipconfigs);
  } else {
    LOG(WARNING) << path_ << ": GetDeviceProperties: no IPConfigs property";
  }
  // don't expect to find Cellular.* properties if we're not a cellular device
  if (type_ != kTypeCellular) {
    return true;
  }
  it = properties.find(kFlimflamDeviceCarrierProperty);
  if (it != properties.end()) {
    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
    OnCarrierUpdate(value.reader().get_string());
  } else {
    LOG(WARNING) << path_
        << ": GetDeviceProperties: no Cellular.Carrier property";
  }
  return true;
}

bool DeviceImpl::CreateByteCounter() {
  if (byte_counter_ != NULL) {
    LOG(WARNING) << path_ << ": CreateByteCounter: counter already exists";
    return false;
  }
  if (interface_.empty()) {
    LOG(WARNING) << path_ << ": CreateByteCounter: interface name unknown";
    return false;
  }
  byte_counter_ = ByteCounter::NewByteCounter(interface_);
  if (byte_counter_ == NULL) {
    LOG(ERROR) << path_
        << ": CreateByteCounter: could not create byte counter for "
        << interface_;
    return false;
  }
  byte_counter_->SetDelegate(this);
  return true;
}

void DeviceImpl::DeleteByteCounter() {
  LOG(INFO) << path_ << ": DeleteByteCounter";
  if (byte_counter_ != NULL) {
    delete byte_counter_;
    byte_counter_ = NULL;
  }
}

}  // namespace cashew
